/************************************************************************

								Heroes IV
					  Copyright 2000, The 3DO Company

	------------------------------------------------------------------
  						      resource_file_dir.cpp

	$Header: /resource_editor/resource_file_dir.cpp $

	$NoKeywords: $

 ************************************************************************/

// kill the 255+ character identifier warning.  Templates tend to exceed that easily.
#include "precompiled.h"
#pragma warning( disable: 4786 )

#include "resource_file_dir.h"

#include <algorithm>
#include <assert.h>
#include <sstream>
#include "memory_buffer.h"
#include "streambuf_operators.h"
#include "string_insensitive_compare.h"

int const t_resource_file_dir::k_version = 5;
static char const k_signature[] = "H4R";

int const k_file_header_size = 8;

namespace
{
	class t_sort_by_position
	{
	public:
		bool operator ()(t_resource_dir_entry const& left, t_resource_dir_entry const& right) const
		{
			return left.data_position < right.data_position;
		}
	};
};

// --------------------------------------------------------
// constructor
// --------------------------------------------------------
t_resource_file_dir::t_resource_file_dir()
{
	clear();
}

// --------------------------------------------------------
// add a new directory entry
// --------------------------------------------------------
void t_resource_file_dir::add( t_resource_dir_entry& arg )
{
	t_resource_dir_entry entry = arg;

	entry.data_position = 0;
	entry.set_length( 0 );
    m_directory.push_back( entry );
}

// --------------------------------------------------------
// reset state
// --------------------------------------------------------
void t_resource_file_dir::clear()
{
    m_directory.clear();
	m_empty_blocks.clear();
	m_version = k_version;
	m_directory_start = k_file_header_size;
	m_directory_length = 0;
	
	t_empty_block block;

	// add a single empty block for everything.
	block.start = k_file_header_size;
	block.length = 1000000000;
	m_empty_blocks.push_back( block );
}



// --------------------------------------------------------
// read file directory from resource
// --------------------------------------------------------
bool t_resource_file_dir::read( std::streambuf& buffer )
{
    char signature[3]; 
    int  size;
    t_resource_dir_entry entry;

	entry.data_type = k_medium_compression_binary;

	clear();
	buffer.pubseekpos( 0 );
    ::read( buffer, signature, sizeof(signature) );
    if (memcmp( signature, k_signature, sizeof(signature) ) != 0)
        return false;
    m_version = buffer.sbumpc();
	if (m_version > k_version)
		return false;
	if (m_version < 2) // version 1 files are no longer supported
		return false;
	m_directory_start = get<int>(buffer);
    buffer.pubseekpos( m_directory_start );

    buffer >> size;
	m_directory.reserve( size );
    while (size--)
    {
        buffer >> entry.data_position;
		entry.set_length( get<int>(buffer) );
		if (m_version < 4)
			entry.set_uncompressed_size( 0 );
		else
			entry.set_uncompressed_size( get<t_uint32>( buffer ));
         ::read( buffer, &entry.file_time, sizeof( entry.file_time ));

        if (!read_string16( buffer, entry.name ))
            return false;
        if (!read_string16( buffer, entry.directory ))
            return false;
		// version 2 files didn't have aliases.
		if (m_version < 3)
			entry.aliased_name = "";
		else
		{
			if (!read_string16( buffer, entry.aliased_name ))
				return false;
		}
		if (entry.aliased_name.empty())
			allocate( entry.data_position, entry.get_length() );
		else
		{
			entry.data_position = 0;
			entry.set_length( 0 );
		}
		
		if (m_version > 4)
		{
			entry.data_type = get<t_uint32>( buffer );
		}

		m_directory.push_back( entry );
    } 
	m_directory_length = tell( buffer ) - m_directory_start;
	allocate( m_directory_start, m_directory_length );

	// prior to version 4, uncompressed size was not stored
	if (m_version < 4)
	{
		int i;
		for (i = 0; i < m_directory.size(); i++)
		{
			if (m_directory[i].aliased_name.empty())
			{
				buffer.pubseekpos( m_directory[i].data_position + m_directory[i].get_length() - 4 );
				m_directory[i].set_uncompressed_size( get<t_uint32>( buffer ) );
			}
		}
	}
    return true;
}

// --------------------------------------------------------
// find blocks which are empty
// --------------------------------------------------------
void t_resource_file_dir::allocate( int start, int size )
{
	if (size == 0)
		return;

	// find the empty block from which we wish to remove data.
	std::list<t_empty_block>::iterator block_index = m_empty_blocks.begin();
	t_empty_block old_block;

	while (block_index != m_empty_blocks.end() && block_index->start <= start)
	{
		old_block = *block_index;
		block_index++;
	}

	if (block_index != m_empty_blocks.begin())
		block_index--;

	bool          erase_block = true;
	t_empty_block empty_block;

	old_block = *block_index;
	assert( old_block.start + old_block.length >= start + size );

	// save the empty block after the directory
	empty_block.start = start + size;
	empty_block.length = old_block.start + old_block.length - empty_block.start;
	if (empty_block.length > 0)
	{
		*block_index = empty_block;
		erase_block = false;
	}

	// save the empty block before the directory
	empty_block.start = old_block.start;
	empty_block.length = start - empty_block.start;
	if (empty_block.length > 0)
	{
		if (!erase_block)
			m_empty_blocks.insert( block_index, empty_block );
		else
		{
			*block_index = empty_block;
			erase_block = false;
		}
	}

	// erase the old block
	if (erase_block)
		m_empty_blocks.erase( block_index );
}

// --------------------------------------------------------
// allocate a block of disk space
// --------------------------------------------------------
int t_resource_file_dir::allocate( int size )
{
	std::list<t_empty_block>::iterator index = m_empty_blocks.begin();

	while (index != m_empty_blocks.end())
	{
		if (index->length >= size)
		{
			int start = index->start;

			index->length -= size;
			index->start += size;
			if (index->length == 0)
				m_empty_blocks.erase( index );
			return start;
		}
		index++;
	}
	assert( false );
	return -1;
}

// --------------------------------------------------------
// free a block of disk space
// --------------------------------------------------------
void t_resource_file_dir::dispose( int start, int length )
{
	if (length == 0)
		return;

	// don't free bytes inside the header.
	if (start < k_file_header_size)
	{
		length -= k_file_header_size - start;
		start = k_file_header_size;
	}

	std::list<t_empty_block>::iterator index = m_empty_blocks.begin();

	// find the empty block right after the newly freed block
	while (index != m_empty_blocks.end() && index->start < start)
		index++;

	t_empty_block empty_block;

	assert( index != m_empty_blocks.end() );
	assert( start + length <= index->start );

	// see if we should pre-pend this to the block.
	if (start + length == index->start )
	{
		index->start = start;
		index->length += length;
		empty_block = *index;
	}
	else
	{
		empty_block.start = start;
		empty_block.length = length;
		m_empty_blocks.insert( index, empty_block );
		index--;
	}
	// see if we should merge this block with the previous block.
	if (index == m_empty_blocks.begin())
		return;

	index--;
	if (index->start + index->length == empty_block.start)
	{
		index->length += empty_block.length;
		index++;
		m_empty_blocks.erase( index );
	}
}

// --------------------------------------------------------
// write file directory to resource file
// --------------------------------------------------------
bool t_resource_file_dir::write( std::streambuf& file ) 
{
    int index;

	file.pubseekpos( 0 );
    ::write( file, k_signature, 3 );
    file.sputc( k_version );

	t_memory_buffer buffer;

    buffer << m_directory.size();
    for (index = 0; index < m_directory.size(); index++)
    {
        buffer << m_directory[index].data_position;
		buffer << m_directory[index].get_length();
		put<t_uint32>( buffer, m_directory[index].get_uncompressed_size() );
        ::write( buffer, &m_directory[index].file_time, sizeof( m_directory[index].file_time ));
        if (!write_string16( buffer, m_directory[index].name ))
            return false;
        if (!write_string16( buffer, m_directory[index].directory ))
            return false;
		if (!write_string16( buffer, m_directory[index].aliased_name ))
			return false;
		put<t_int32>( buffer, m_directory[index].data_type );
    }
	
	// free the old directory space
	dispose( m_directory_start, m_directory_length );
	
	std::string data_buffer = buffer.copy_buffer();

	m_directory_length = data_buffer.size();
	m_directory_start = allocate( m_directory_length );
	
	file << m_directory_start;
	file.pubseekpos( m_directory_start );

	::write( file, &(*data_buffer.begin()), m_directory_length );

    return true;
}


// --------------------------------------------------------
// remove item (if it exists)
// --------------------------------------------------------
void t_resource_file_dir::remove( std::string const& name )
{
	std::vector<t_resource_dir_entry>::iterator index;
 
    for (index = m_directory.begin(); index != m_directory.end(); index++)
    {
		if (stricmp( index->name.c_str(), name.c_str() ) == 0)
		{
			assert( index->get_length() == 0 || index->aliased_name.empty() ); 
			dispose( index->data_position, index->get_length() );
			m_directory.erase( index );
			return;
		}
	}
}

// --------------------------------------------------------
// rename item (if it exists)
// --------------------------------------------------------
void t_resource_file_dir::rename( std::string const& old_name, std::string const& name )
{
	std::vector<t_resource_dir_entry>::iterator index;
 
    for (index = m_directory.begin(); index != m_directory.end(); index++)
    {
		if (stricmp( index->name.c_str(), old_name.c_str() ) == 0)
		{
			index->name = name;
			return;
		}
	}
}

// --------------------------------------------------------
// refresh contents of a directory entry
// --------------------------------------------------------
void t_resource_file_dir::update( t_resource_dir_entry const& entry )
{
	std::vector<t_resource_dir_entry>::iterator index;

    for (index = m_directory.begin(); index != m_directory.end(); index++)
    {
		if (stricmp( index->name.c_str(), entry.name.c_str() ) == 0)
			break;
	}

	if (index != m_directory.end())
		*index = entry;
}


// --------------------------------------------------------
// write a new or old item to the resource file
// --------------------------------------------------------
bool t_resource_file_dir::write( std::streambuf& buffer, t_resource_dir_entry& entry,
		                         std::string const& data )
{
	std::vector<t_resource_dir_entry>::iterator index;

    for (index = m_directory.begin(); index != m_directory.end(); index++)
    {
		if (stricmp( index->name.c_str(), entry.name.c_str() ) == 0)
			break;
	}

	if (index == m_directory.end())
	{
		m_directory.push_back( entry );
		index = m_directory.end();
		index--;
	}
	else
	{
		assert( index->get_length() == 0 || index->aliased_name.empty() ); 
		dispose( index->data_position, index->get_length() );
	}

	// check if the file has a header yet.
	if ( m_directory_length == 0 )
	{
		// write the directory header.
		write( buffer );
	}
	
	assert( entry.aliased_name.empty() );

	char const* end_header = &(*(data.begin() + data.size() - 4));

	if (data.size() > 0 && entry.data_type == k_raw_uncompressed_binary)
	{
		entry.set_uncompressed_size( data.size() );
	}
	else if (data.size() > 0 && 
		(
		entry.data_type == k_low_compression_binary		||
		entry.data_type == k_medium_compression_binary	||
		entry.data_type == k_high_compression_binary 
		))
	{
		//read the tail zlib writes, this is very compressor specific

		char const* end_header = &(*(data.begin() + data.size() - 4));
		entry.set_uncompressed_size( *reinterpret_cast<t_uint32 const*>( end_header ) );
	}
	else
	{
		entry.set_uncompressed_size( 0 );
	}
	
	*index = entry;
	index->set_length( data.size() );
	index->data_position = allocate( data.size() );
	int result = buffer.pubseekpos( index->data_position );

	assert( result == index->data_position );
	::write( buffer, &(*data.begin()), data.size() );
	entry = *index;
	return true;
}
